
package com.hockeo.tinygds;

import java.lang.reflect.Field;

import com.google.appengine.api.datastore.DataTypeUtils;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Text;
import com.hockeo.tinygds.annotation.TName;
import com.hockeo.tinygds.error.TNotLoadableException;
import com.hockeo.tinygds.error.TNotPersistableException;
import com.hockeo.tinygds.error.TinyGDSException;
import com.hockeo.tinygds.wrappers.TGAEWrapper;
import com.hockeo.tinygds.wrappers.TGeoPt;

/**
 * Represents a persistable field in a Class that is recognised as TinyGDS entity.
 *
 * @param <T> the type of the underlying field
 * @author jjanke
 */
abstract class TField<T>
{
  private final TinyGDSFactory d_factory;
  private final Field          d_field;
  private final String         d_strPropertyName;

  protected TField( TinyGDSFactory factory, Field field )
  {
    d_factory = factory;

    d_field = field;
    d_field.setAccessible( true );

    // Use the field's name as property name unless a TName annotation is present.
    //
    TName name = field.getAnnotation( TName.class );
    if ( name != null )
    {
      if ( name.value() != null && name.value().length() > 0 )
        d_strPropertyName = name.value();
      else
        throw TNotPersistableException.annotationTNameWithoutValue( field );
    }
    else
      d_strPropertyName = field.getName();
  }

  public String getPropertyName()
  {
    return d_strPropertyName;
  }

  public Field getRealField()
  {
    return d_field;
  }

  protected TinyGDSFactory getTinyGDSFactory()
  {
    return d_factory;
  }

  /**
   * Returns the value in a special customised way that is adapted to the Google
   * Datastore. I.e. a {@link TFieldString} may return a {@link Text} instance instead of
   * a String.
   *
   * By default, this method returns the same as {@link #getValue(Object)}. Subclasses may
   * change this behaviour.
   *
   * @param objInstance the instance from which to retrieve the field value
   * @return the field value in a type that is suitable for the GAE datastore
   */
  public Object getValueForDatastore( Object objInstance )
  {
    return getValue( objInstance );
  }

  @SuppressWarnings( "unchecked" )
  protected T getValue( Object objInstance )
  {
    try
    {
      return (T) d_field.get( objInstance );
    }
    catch ( Exception ex )
    {
      throw new TinyGDSException( "Failed to get value for field " + d_field.getDeclaringClass().getName() + "#"
          + d_field.getName() + " from Object " + objInstance.toString() + ".", ex );
    }
  }

  /**
   * Sets a value received from the Google datastore. The value is either directly set to
   * the underlying field or transformed beforehand to match the field's actual type. For
   * instance a {@link Key} instance may be converted into a {@link TKey} instance before
   * actually setting the field value.
   *
   * @param objInstance the instance to which to set the value
   * @param objDatastoreValue the value to be set
   */
  @SuppressWarnings( "unchecked" )
  public void setValueFromDatastore( Object objInstance, Object objDatastoreValue )
  {
    if ( isDirectlySettable( objDatastoreValue ) )
      setValue( objInstance, (T) objDatastoreValue );
    else
      setValue( objInstance, transformDatastoreValue( objDatastoreValue ) );
  }

  /**
   * Checks if a value retrieved from the Google datastore is directly settable to the
   * field represented by this instance.
   *
   * @param objDatastoreValue the value to be set
   * @return if the value is directly settable <code>true</code>, otherwise
   *         <code>false</code>
   */
  protected boolean isDirectlySettable( Object objDatastoreValue )
  {
    if ( objDatastoreValue == null || getRealField().getType().isAssignableFrom( objDatastoreValue.getClass() ) )
      return true;

    return false;
  }

  /**
   * Transforms a Google datastore value so that it can be set to the underlying field.
   *
   * @param objDatastoreValue the value to be transformed
   * @return the transformed value (by default the original input value, please override
   *         to customise the behaviour)
   * @throws TNotLoadableException if the transformation fails
   */
  @SuppressWarnings( "unchecked" )
  protected T transformDatastoreValue( Object objDatastoreValue )
  {
    try
    {
      return (T) objDatastoreValue;
    }
    catch ( Exception ex )
    {
      throw TNotLoadableException.cannotConvertValue( objDatastoreValue, getRealField() );
    }
  }

  protected void setValue( Object objInstance, T objFieldValue )
  {
    try
    {
      d_field.set( objInstance, objFieldValue );
    }
    catch ( Exception ex )
    {
      throw new TinyGDSException( "Failed to set value " + objFieldValue.toString() + " for field "
          + d_field.getDeclaringClass().getName() + "#" + d_field.getName() + ".", ex );
    }
  }

  static TField<?> createTField( TinyGDSFactory factory, Field field )
  {
    if ( !isSupportedType( field.getType() ) )
      throw TNotPersistableException.unsupportedFieldType( field );

    if ( field.getType().equals( String.class ) )
      return new TFieldString( factory, field );

    if ( isNumber( field.getType() ) )
      return new TFieldNumber( factory, field );

    if ( field.getType().isEnum() )
      return new TFieldEnum( factory, field );

    if ( field.getType().equals( TKey.class ) )
      return new TFieldString( factory, field );

    if ( field.getType().equals( TGeoPt.class ) )
      return new TFieldString( factory, field );

    return new TFieldStandard( factory, field );
  }

  /**
   * Tests whether the class corresponds to a type that is supported for being persisted
   * by the Google datastore.
   *
   * @param clsType the class to be tested
   * @return <code>true</code> if the class can be handled by the Google datastore
   */
  private static boolean isSupportedType( Class<?> clsType )
  {
    return DataTypeUtils.isSupportedType( clsType ) || clsType == Enum.class || clsType == TKey.class
        || clsType.isPrimitive() || clsType == byte[].class || clsType == TGAEWrapper.class;
  }

  private static boolean isNumber( Class<?> cls )
  {
    if ( Number.class.isAssignableFrom( cls ) )
      return true;

    return cls.equals( Byte.TYPE ) || cls.equals( Short.TYPE ) || cls.equals( Integer.TYPE ) || cls.equals( Long.TYPE )
        || cls.equals( Float.TYPE ) || cls.equals( Double.TYPE );
  }
}
